﻿package de.gameduell.framework.game.billard.controller {
	import de.gameduell.framework.game.billard.model.Ball;
	import de.gameduell.framework.game.billard.model.Cushion;
	import de.gameduell.framework.game.billard.model.Hole;
	
	import org.cove.ape.APEngine;
	import org.cove.ape.AbstractParticle;
	import org.cove.ape.CollisionDetector;
	import org.cove.ape.Group;
	import org.cove.ape.RectangleParticle;
	import org.cove.ape.SphereParticle;
	import org.cove.ape.Vector;
	import org.cove.ape.Vector3f;
	
	import flash.geom.Point;
	import flash.geom.Rectangle;	

	/**
	 * @author cbra
	 */
	public class BillardEngine extends APEngine {

		private const PRECISION:int = 16;
		
		private var _balls:Array;
		private var holes:Array;
		private var minRadius:Number = Infinity;
		private var maxSpeed:Number = 1;
		private var _whiteBall:Ball;
		private var _group:Group;	
		private var movingBalls:Array;
		private var restingParticles:Array;
		private var _cushions:Array;
		private var _cushionLessRectangle:Rectangle;
		private var _firstPocketedBall:Ball;
		private var savedSpeed:Number;

		public function BillardEngine() {
			super();
			_balls = new Array();
			_cushions = new Array();
			holes = new Array();
			movingBalls = new Array();
			restingParticles = new Array();
			_group = new Group(this, true);
			_firstPocketedBall = null;
			_cushionLessRectangle = new Rectangle();
			savedSpeed = 0;
		}

		public function copyBillardEngine(billardEngine:BillardEngine):void {
			copyAPEngine(billardEngine);
			_balls    = getBalls();
			_cushions = getCushions();
			_whiteBall =_balls[0];
			minRadius  = billardEngine.minRadius;
			maxSpeed   = billardEngine.maxSpeed;
			_group     = groups[0];
			_cushionLessRectangle = billardEngine._cushionLessRectangle;
		}
		
		public function clearRuleInfo():void {
			_firstPocketedBall = null;
		}

		public function cloneBillardEngine():BillardEngine {
			var clone:BillardEngine = new BillardEngine();
			clone.copyBillardEngine(this);
			return clone;
		}
		
		override public function addGroup(g:Group):void {
			super.addGroup(g);
			_group = g;
		}
		
		public function addBall(ball:Ball):void {
			_group.addParticle(ball);
			_balls.push(ball);
			if(_balls.length == 1){
				_whiteBall = ball;
				minRadius  = _whiteBall.radius;
				maxSpeed   = minRadius / PRECISION; 
			}
		}
		
		public function set cushionLessRectangle(aRect:Rectangle):void {
			_cushionLessRectangle = aRect;
		}
		
		public function removeBallById(id:int):void {
			for(var i:* in balls){
				var ball:Ball = balls[i];
				if(ball.id == id){
					_balls.splice(i,1);
					_group.removeParticle(ball);
				}		
			}
		}
		
		public function addCushion(cushion:RectangleParticle):void {
			_group.addParticle(cushion);
			_cushions.push(cushion);
		}
		
		public function addHole(hole:Hole):void {
			holes.push(hole);
		}
		
		public function removeBalls():void{
			while(_balls.length > 0){
				_group.removeParticle(_balls[0]);
				_balls.splice(0,1);		
			}
		}
				
		public function stepOptimized():void {
			//Debug.trace("MAJORSTEP");

			findMovingBalls(true);
			updateMovingBalls();
			optimizedCheck();
			
			if(checkSphereCollisions()){
				optimizedSubStep();
			}
			createSteps();	
		}
		
		private function findMovingBalls(onMajorStep:Boolean):void{
			movingBalls.length = 0;
			restingParticles.length = 0;
			for(var i:* in _balls){
				var ball:Ball = _balls[i];
				
				// sub + major step
				ball.onMajorFrame = onMajorStep;
				if (onMajorStep) {
					ball.collidedParticles.length = 0;		
//					ball.resetCollisionFlags();
				}		
				// ensure that ball is currently in this engine
				if(ball.apEngine == this && ball.collidable){
					if(!ball.isStopped() && !ball.fixed && !ball.removed){
						movingBalls.push(ball);
					}else{
						restingParticles.push(ball);			
					}
				}			
			}
			restingParticles = restingParticles.concat(_cushions);
		}
		
		private function updateMovingBalls():void{
			for each(var ball:Ball in movingBalls){
				ball.update(1,true);
			}
		}

		protected function createSteps():void {	
			for (var i:int = 0;i < _balls.length; i++) {
				var ball:Ball = _balls[i];
				if(!ball.fixed && !ball.removed){
					ball.createStep();
				}
			}			
		}
		
		public function ballsStopped():Boolean {
			return movingBalls.length == 0;
		}	
		
		// not supported in combination with gravity ()
		public function optimizedCheck():void { 				
			// outer loop : movingPartice (could be changed in inner loop -> static by iterator		
			var movingOtherIndex:int = 1;
			var dx:Number = 0;
			var dy:Number = 0;
			for (var movingIndex:int=0; movingIndex<movingBalls.length; movingIndex++) {
				var movingBall:Ball = movingBalls[movingIndex];
				
				// inner loop against other moving (one times, self excluded)
				for (var i:int=movingOtherIndex; i < movingBalls.length; i++) {
					var otherMovingParticle:Ball = movingBalls[i];
					
					// avoid double check after change in inner loop
					if (movingBall != otherMovingParticle) {
						dx = Math.abs(movingBall.curr.x - otherMovingParticle.curr.x);
						dy = Math.abs(movingBall.curr.y - otherMovingParticle.curr.y);
						if(dx < 20 && dy < 20){			
							CollisionDetector.test(movingBall, otherMovingParticle);
						}
					}
					
				}
	
				// inner loop against resting (every time)
				for (var restingIndex:int=0; restingIndex < restingParticles.length; restingIndex++) {
					var otherRestingParticle:AbstractParticle = restingParticles[restingIndex];
					
					// avoid double check after change in inner loop
					if (movingBall != otherRestingParticle) {
						dx = Math.abs(movingBall.curr.x - otherRestingParticle.curr.x);
						dy = Math.abs(movingBall.curr.y - otherRestingParticle.curr.y);
						if(otherRestingParticle is Ball){
							if(dx < 20 && dy < 20 ){						
								CollisionDetector.test(movingBall, otherRestingParticle);
							}
						}else if(otherRestingParticle is Cushion){
							var movingParticlePos:Point = new Point(movingBall.curr.x,movingBall.curr.y);
							if(!_cushionLessRectangle.containsPoint(movingParticlePos)){
								var collidedWithCushion:Boolean = false;
								if(movingBall.collidedParticles.length > 0) {
									for each(var p in movingBall.collidedParticles){
										if(p is Cushion){
											collidedWithCushion = true;
										}					
									}
								}
								// avoid double collision on corners
								if(!collidedWithCushion){
									var cushion:Cushion = Cushion(otherRestingParticle);
									if(cushion.extendedBoundingBox.containsPoint(movingParticlePos)){
										CollisionDetector.test(movingBall, otherRestingParticle);
									}
								}
							}
						}
					}
				}
				
				// increment other statring offset
				movingOtherIndex++;
			}
			
//			Debug.trace("after optimizedCheck() " + _whiteBall.toString() + " collidedParticles=" + _whiteBall.collidedParticles);
		}
		
		private function optimizedSubStep():void {
//			Debug.trace("before optimizedSubStep() " + _whiteBall.toString());
			
			// find moving ball and calculate max speed (on major steps)
			findMovingBalls(true);			
			var currMaxSpeed:Number = getBallMaxSpeed();
			
			var adaptivStepSize:int = Math.max(1, currMaxSpeed / maxSpeed);		
			if(adaptivStepSize > 1) {
				_dt = 1 / adaptivStepSize;
				var currSphere:SphereParticle;
				
//				Debug.trace("before ball_2 velocity: "+_balls[2].velocity.magnitude() + " curr=" + _balls[2].curr + " lastCurr=" + _balls[2].lastCurr + " prev=" + _balls[2].prev + " lastPrev=" + _balls[2].lastPrev);
				
				// back step + check for moving (on sub steps)
				backStep();
				
//				Debug.trace("after ball_2 velocity: "+_balls[2].velocity.magnitude() + " curr=" + _balls[2].curr + " lastCurr=" + _balls[2].lastCurr + " prev=" + _balls[2].prev + " lastPrev=" + _balls[2].lastPrev);
				
//				var movingBalls:Array = _balls;
				// scale to small step
				for(var i:* in movingBalls) {
					currSphere = movingBalls[i];
					currSphere.velocity = currSphere.velocity.mult(_dt);
					currSphere.spin = currSphere.spin.multScalar(_dt);
				}
				
				// +1 => one step more
				for (var j:int = 0; j < adaptivStepSize + 1; j++) {
					//Debug.trace("SUBSTEP");

//					integrate(_dt, j == 0);	
					findMovingBalls(false);					
					for(var l in movingBalls){				
						movingBalls[l].update(1,false);
					}	
//					updateMovingBalls();	
					optimizedCheck();
//					checkCollisions();
				}	
				// scale back to big step	
				for(var k in movingBalls) {
					currSphere = movingBalls[k];
					currSphere.velocity = currSphere.velocity.mult(adaptivStepSize);
					currSphere.spin = currSphere.spin.multScalar(adaptivStepSize);
				}
				_dt = 1;
			}		
			
//			Debug.trace("after optimizedSubStep() " + _whiteBall.toString());
			
		}	
		
		public function getBallMaxSpeed():Number {
//			var movingBalls:Array = _balls;	
			var max:Number = 0;
			for(var i:* in movingBalls) {
				max = Math.max(max,SphereParticle(movingBalls[i]).velocity.magnitude());
			}
			return max;
		}
		
		private function checkSphereCollisions():Boolean{
//			var movingBalls:Array = _balls;
			for(var i:* in movingBalls){
				var ball:Ball = movingBalls[i];
				var collidedparticles:Array = ball.collidedParticles;
				if(!ball.isStopped()){
					for(var j in holes){
						var hole:Hole = holes[j];
						if(ball.position.distance(hole.position) < hole.radius){					
							ball.collidable = false;
							ball.fixed = true;
							ball.hole = hole;
							if(_firstPocketedBall == null){
								_firstPocketedBall = ball;
							}
//							Debug.trace("checkSphereCollisions ball in hole "+ball.id);
						}
						
					}
				}		
				if(collidedparticles.length > 0){		
					return true;
				}
			}
			return false;
		}
		
		public function get firstPocketedBall():Ball{
			return _firstPocketedBall;
		}
		
		public function get balls():Array{
			return _balls;
		}
		
		public function get cushions():Array{
			return _cushions;
		}
		
		public function get whiteBall():Ball{
			return _group.particles[0];
		}
		
		public function get group():Group{
			return _group;
		}
		
		public function synchronizeBalls(engine:BillardEngine):void{
			var mySpheres:Array = getBalls();
			var otherSpheres:Array = engine.getBalls();
			
			for(var i:* in mySpheres){
				var mySphere:Ball = mySpheres[i];
				var otherSphere:Ball = otherSpheres[i];
				mySphere.copySphere(otherSphere);
			}	
			
		}
		
		public function getBalls():Array{
			var _balls:Array = new Array();
			for(var i:* in groups){
				var currGroup:Group = Group(groups[i]);
				for(var j in currGroup.particles){
					var currParticle:AbstractParticle = currGroup.particles[j];
					if(currParticle is Ball){
						_balls.push(currParticle);
					}	
				}
			}
			return _balls;
		}
		
		public function getCushions():Array{
			var cushions:Array = new Array();
			for(var i:* in groups){
				var currGroup:Group = Group(groups[i]);
				for(var j in currGroup.particles){
					var currParticle:AbstractParticle = currGroup.particles[j];
					if(currParticle is RectangleParticle) {
						cushions.push(currParticle);
					}	
				}
			}
			return cushions;
		}
		
		override public function backStep():void{
//			Debug.trace("before backStep() " + _whiteBall.toString());
			super.backStep();
//			Debug.trace("after backStep() " + _whiteBall.toString());
			
		}
	
	}
}
